from tkinter import messagebox
from graphics import *
import heapq
import random

GRID_WIDTH = 40  # 棋盘格的宽高

COLUMN = 15  # 总列数
ROW = 15  # 总行数

list1 = []  # AI的所有落子
list2 = []  # 人类的所有落子
list3 = []  # 所有落子

list_all = []  # 整个棋盘的所有点

ratio = 0.1  # 进攻的系数    小于1 进攻型  大于1 防守型

# 棋型的评估分数,例：落子为* * 1 1 0 0 0 * * 时的棋型得10分 1为计算分数的对象的棋子,0为可落子的空位置
shape_score = [(10, (1, 1, 0, 0, 0)),
               (10, (1, 0, 1, 0, 0)),
               (10, (1, 0, 0, 1, 0)),
               (50, (0, 1, 1, 0, 0)),
               (50, (0, 1, 0, 1, 0)),
               (100, (1, 1, 0, 1, 0)),
               (100, (0, 0, 1, 1, 1)),
               (100, (1, 1, 1, 0, 0)),
               (500, (0, 1, 0, 1, 1, 0)),
               (500, (0, 1, 1, 0, 1, 0)),
               (2000, (0, 1, 1, 1, 0)),
               (4000, (1, 1, 1, 0, 1)),
               (4000, (1, 1, 0, 1, 1)),
               (4000, (1, 0, 1, 1, 1)),
               (5000, (1, 1, 1, 1, 0)),
               (5000, (0, 1, 1, 1, 1)),
               (100000, (0, 1, 1, 1, 1, 0)),
               (99999999, (1, 1, 1, 1, 1))]
# 定义A*搜索中的每个方向
DIRECTIONS = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]


def a_star_search():
    open_list = []  # 所有带搜索的点
    closed_set = {}  # 记录已经搜索过的点以及其价值评估分数
    for point in list3:
        neighbors = get_neighbors(point)  # 取出所有落子的邻接点
        for neighbor in neighbors:
            if neighbor in list_all and neighbor not in list3 and neighbor not in closed_set.keys():  # 修改此处检查邻居是否在 closed_set 的键中
                # 后续两个append()和remove()是为了评估落下评估点后的棋局状态(但实际还未落下)，所以需要先添加进两个list中，之后再删除
                list1.append(neighbor)  # 将邻居加入AI列表
                list2.append(neighbor)  # 将邻居加入人类落子列表
                if neighbor not in [node for (_, node) in open_list]:  # 检查节点是否已经存在于open列表中
                    heapq.heappush(open_list, (-evaluation(True), neighbor))  # 将当前点加入open列表
                # 从列表中删除刚刚加入的邻居
                list1.remove(neighbor)
                list2.remove(neighbor)

    if not open_list:
        return None

    while open_list:
        # 在a_star_search函数中修改取出具有最小代价的节点的行为
        min_cost = min(open_list)[0]  # 获取当前最小代价
        min_cost_nodes = [node for cost, node in open_list if cost == min_cost]  # 找到所有具有最小代价的节点列表
        current_node = random.choice(min_cost_nodes)  # 从具有相同最小代价的节点列表中随机选择一个节点
        open_list.remove((min_cost, current_node))  # 从open_list中移除选择的节点
        current_cost = min_cost

        if current_node not in closed_set:
            if current_node not in list3:
                closed_set[current_node] = current_cost  # 记录当前点和评估分数

            if len(closed_set) >= DEPTH:  # 到达搜索深度
                max_score_node = min(closed_set, key=closed_set.get)  # 找到评估分数最大的点(代价最小,即价值最大)
                return max_score_node

            neighbors = get_neighbors(current_node)
            for neighbor in neighbors:
                if neighbor in list_all and neighbor not in list3 and neighbor not in closed_set.keys():  # 修改此处检查邻居是否在 closed_set 的键中
                    list1.append(neighbor)  # 将邻居加入AI列表
                    list2.append(neighbor)  # 将邻居加入列表
                    if neighbor not in [node for (_, node) in open_list]:  # 检查节点是否不在open列表中
                        heapq.heappush(open_list, (-evaluation(True), neighbor))  # 将节点推入open列表
                    # 从列表中删除刚刚加入的邻居
                    list1.remove(neighbor)
                    list2.remove(neighbor)
    # 如果搜索完所有可搜索的点时仍未到达搜索深度，则返回评估分数最大的点
    max_score_node = min(closed_set, key=closed_set.get)
    return max_score_node


# 获取一个点的所有邻接点
def get_neighbors(point):
    neighbors = []
    for direction in DIRECTIONS:
        neighbor = (point[0] + direction[0], point[1] + direction[1])
        neighbors.append(neighbor)
    return neighbors


# 一个点的价值评估函数
def evaluation(is_ai):
    if is_ai:
        my_list = list1
        enemy_list = list2
    else:
        my_list = list2
        enemy_list = list1

    # 算自己的得分
    score_all_arr = []  # 得分形状的位置,避免重复计算
    my_score = 0
    for pt in my_list:  # 计算自己的所有点在四个方向上的得分情况
        m = pt[0]
        n = pt[1]
        my_score += cal_score(m, n, 0, 1, enemy_list, my_list, score_all_arr)
        my_score += cal_score(m, n, 1, 0, enemy_list, my_list, score_all_arr)
        my_score += cal_score(m, n, 1, 1, enemy_list, my_list, score_all_arr)
        my_score += cal_score(m, n, -1, 1, enemy_list, my_list, score_all_arr)

    #  算敌人的得分
    score_all_arr_enemy = []
    enemy_score = 0
    for pt in enemy_list:
        m = pt[0]
        n = pt[1]
        enemy_score += cal_score(m, n, 0, 1, my_list, enemy_list, score_all_arr_enemy)
        enemy_score += cal_score(m, n, 1, 0, my_list, enemy_list, score_all_arr_enemy)
        enemy_score += cal_score(m, n, 1, 1, my_list, enemy_list, score_all_arr_enemy)
        enemy_score += cal_score(m, n, -1, 1, my_list, enemy_list, score_all_arr_enemy)

    total_score = my_score + enemy_score * ratio

    return total_score


# 一个方向上的分值计算
def cal_score(m, n, x_direct, y_direct, enemy_list, my_list, score_all_arr):
    add_score = 0  # 加分项
    # 在一个方向上， 只取最大的得分项
    max_score_shape = (0, None)

    # 如果此方向上，该点已经有得分形状，不重复计算
    for item in score_all_arr:
        for pt in item[1]:
            if m == pt[0] and n == pt[1] and x_direct == item[2][0] and y_direct == item[2][1]:
                return 0

    # 在落子点 左右方向上循环查找得分形状
    for offset in range(-5, 1):
        # offset = -2
        pos = []
        found_valid_pos = False  # 布尔变量用于记录是否找到有效位置
        for i in range(0, 6):
            next_pos = (m + (i + offset) * x_direct, n + (i + offset) * y_direct)
            if not (0 <= next_pos[0] <= COLUMN and 0 <= next_pos[1] <= ROW) and i != 5:
                found_valid_pos = True  # 如果存在不满足条件的情况，设置布尔变量为 True
                break
            if next_pos in my_list:
                pos.append(1)
            elif next_pos in enemy_list:
                pos.append(2)
            else:
                pos.append(0)
        if found_valid_pos:  # 检查布尔变量是否为真
            continue  # 如果找到无效位置，跳出内层循环
        tmp_shap5 = (pos[0], pos[1], pos[2], pos[3], pos[4])
        tmp_shap6 = (pos[0], pos[1], pos[2], pos[3], pos[4], pos[5])

        for (score, shape) in shape_score:
            if tmp_shap5 == shape or tmp_shap6 == shape:
                if tmp_shap5 == (1, 1, 1, 1, 1):
                    print('到达赛点咯!')
                if score > max_score_shape[0]:  # 棋型记录
                    max_score_shape = (score, ((m + (0 + offset) * x_direct, n + (0 + offset) * y_direct),
                                               (m + (1 + offset) * x_direct, n + (1 + offset) * y_direct),
                                               (m + (2 + offset) * x_direct, n + (2 + offset) * y_direct),
                                               (m + (3 + offset) * x_direct, n + (3 + offset) * y_direct),
                                               (m + (4 + offset) * x_direct, n + (4 + offset) * y_direct)),
                                       (x_direct, y_direct))

    # 计算两个形状相交， 如两个3活 相交， 得分增加 一个子的除外（该代码为原作者代码，此处暂做保留，但码者并未详看，所以暂不做注解）
    if max_score_shape[1] is not None:
        for item in score_all_arr:
            for pt1 in item[1]:
                for pt2 in max_score_shape[1]:
                    if pt1 == pt2 and max_score_shape[0] > 10 and item[0] > 10:
                        add_score += item[0] + max_score_shape[0]

        score_all_arr.append(max_score_shape)

    return add_score + max_score_shape[0]


def game_win(board):
    # 检查行
    for row in range(ROW + 1):
        for col in range(COLUMN - 3):
            if all((col + i, row) in board for i in range(5)):
                return True

    # 检查列
    for col in range(COLUMN + 1):
        for row in range(ROW - 3):
            if all((col, row + i) in board for i in range(5)):
                return True

    # 检查主对角线 \
    for row in range(ROW - 3):
        for col in range(COLUMN - 3):
            if all((col + i, row + i) in board for i in range(5)):
                return True

    # 检查副对角线 /
    for row in range(4, ROW + 1):
        for col in range(COLUMN - 3):
            if all((col + i, row - i) in board for i in range(5)):
                return True

    return False


EXTRA_HEIGHT = 40  # 额外部分的高度


def gomokuwin():
    # 创建窗口
    win = GraphWin("五子棋游戏", GRID_WIDTH * COLUMN, GRID_WIDTH * ROW + EXTRA_HEIGHT)

    # 绘制额外部分
    extra_part = Rectangle(Point(0, 0), Point(COLUMN * GRID_WIDTH, EXTRA_HEIGHT))
    extra_part.setFill('light blue')
    extra_part.draw(win)

    # 绘制按钮
    reset_button = Rectangle(Point(10, 10), Point(90, EXTRA_HEIGHT - 10))
    reset_button.setFill('light gray')
    reset_button.draw(win)
    reset_text = Text(Point(50, EXTRA_HEIGHT / 2), "重置游戏")
    reset_text.draw(win)

    undo_button = Rectangle(Point(110, 10), Point(190, EXTRA_HEIGHT - 10))
    undo_button.setFill('light gray')
    undo_button.draw(win)
    undo_text = Text(Point(150, EXTRA_HEIGHT / 2), "悔棋")
    undo_text.draw(win)

    quit_button = Rectangle(Point(210, 10), Point(290, EXTRA_HEIGHT - 10))
    quit_button.setFill('light gray')
    quit_button.draw(win)
    quit_text = Text(Point(250, EXTRA_HEIGHT / 2), "退出游戏")
    quit_text.draw(win)

    # 绘制棋盘的水平线条
    for j in range(ROW + 1):
        line_horizontal = Line(Point(0, j * GRID_WIDTH + EXTRA_HEIGHT),
                               Point(COLUMN * GRID_WIDTH, j * GRID_WIDTH + EXTRA_HEIGHT))
        line_horizontal.draw(win)

    # 绘制棋盘的垂直线条
    for i in range(COLUMN + 1):
        line_vertical = Line(Point(i * GRID_WIDTH, EXTRA_HEIGHT),
                             Point(i * GRID_WIDTH, ROW * GRID_WIDTH + EXTRA_HEIGHT))
        line_vertical.draw(win)

    return win


def set_difficulty(win):
    global DEPTH  # 搜索深度
    button_width = 80
    button_height = 20
    button_spacing = 100
    button_y = win.getHeight() / 2 - button_height / 2

    buttons = []
    button_labels = ["简单", "中等", "高级"]
    depths = [1, (ROW + 1) * (COLUMN + 1) * 0.3, (ROW + 1) * (COLUMN + 1)]
    button_x1 = (win.getWidth() - 3 * button_spacing) / 2

    for label, depth in zip(button_labels, depths):  # 显示按钮
        button_x2 = button_x1 + button_width
        button = Rectangle(Point(button_x1, button_y), Point(button_x2, button_y + button_height))
        button.setFill('light gray')
        button_text = Text(Point((button_x1 + button_x2) / 2, button_y + button_height / 2), label)
        button.draw(win)
        button_text.draw(win)
        buttons.append((button, button_text, depth))
        button_x1 += button_spacing

    clicked = False
    while not clicked:
        click_point = win.getMouse()
        x = click_point.getX()
        y = click_point.getY()

        for button, _, depth in buttons:  # 遍历所有难度按钮
            button_x1, button_x2 = button.getP1().getX(), button.getP2().getX()
            button_y1, button_y2 = button.getP1().getY(), button.getP2().getY()
            if button_x1 < x < button_x2 and button_y1 < y < button_y2:  # 判断是否点击的当前按钮
                DEPTH = depth
                clicked = True
                break
        if 210 <= x <= 290 and 10 <= y <= EXTRA_HEIGHT - 10:  # 玩家点击了退出按钮
            win.close()
            return True

    for button, button_text, _ in buttons:
        button.undraw()  # 删除按钮
        button_text.undraw()  # 删除按钮文本

    return False


def reset_game(win):
    # 清空棋盘列表和图形
    list1.clear()
    list2.clear()
    list3.clear()
    for item in win.items[:]:
        item.undraw()

    # 重新绘制额外部分
    extra_part = Rectangle(Point(0, 0), Point(COLUMN * GRID_WIDTH, EXTRA_HEIGHT))
    extra_part.setFill('light blue')
    extra_part.draw(win)

    # 重新绘制按钮
    reset_button = Rectangle(Point(10, 10), Point(90, EXTRA_HEIGHT - 10))
    reset_button.setFill('light gray')
    reset_button.draw(win)
    reset_text = Text(Point(50, EXTRA_HEIGHT / 2), "重置游戏")
    reset_text.draw(win)

    undo_button = Rectangle(Point(110, 10), Point(190, EXTRA_HEIGHT - 10))
    undo_button.setFill('light gray')
    undo_button.draw(win)
    undo_text = Text(Point(150, EXTRA_HEIGHT / 2), "悔棋")
    undo_text.draw(win)

    quit_button = Rectangle(Point(210, 10), Point(290, EXTRA_HEIGHT - 10))
    quit_button.setFill('light gray')
    quit_button.draw(win)
    quit_text = Text(Point(250, EXTRA_HEIGHT / 2), "退出游戏")
    quit_text.draw(win)

    # 重新绘制棋盘的水平线条
    for j in range(ROW + 1):
        line_horizontal = Line(Point(0, j * GRID_WIDTH + EXTRA_HEIGHT),
                               Point(COLUMN * GRID_WIDTH, j * GRID_WIDTH + EXTRA_HEIGHT))
        line_horizontal.draw(win)

    # 重新绘制棋盘的垂直线条
    for i in range(COLUMN + 1):
        line_vertical = Line(Point(i * GRID_WIDTH, EXTRA_HEIGHT),
                             Point(i * GRID_WIDTH, ROW * GRID_WIDTH + EXTRA_HEIGHT))
        line_vertical.draw(win)
    return set_difficulty(win)


def find_message(win, message_text):
    for item in win.items[:]:  # 遍历窗口中的所有对象
        if isinstance(item, Text) and item.getText() == message_text:  # 检查是否是 Text 对象且文本内容匹配
            return item  # 如果找到匹配的消息，返回该对象
    return None  # 如果未找到匹配的消息，返回 None


move_hint = None  # 全局变量用于存储落子提示的圆圈
player_wins = False


def undo_move(win):
    global move_hint
    global player_wins

    # 移除当前的红色圆圈（如果存在）
    if move_hint:
        move_hint.undraw()

    if list1 and not player_wins:
        last_ai_move = list1.pop()
        list3.remove(last_ai_move)
        x, y = last_ai_move
        for item in win.items[:]:
            if isinstance(item, Circle):
                if item.getCenter().getX() == x * GRID_WIDTH and item.getCenter().getY() == y * GRID_WIDTH + EXTRA_HEIGHT:
                    item.undraw()
                    break

    if list2:
        last_human_move = list2.pop()
        list3.remove(last_human_move)
        x, y = last_human_move
        for item in win.items[:]:
            if isinstance(item, Circle):
                if item.getCenter().getX() == x * GRID_WIDTH and item.getCenter().getY() == y * GRID_WIDTH + EXTRA_HEIGHT:
                    item.undraw()
                    break
    # 如果还有AI的落子记录，更新红色圆圈的位置
    if list1:
        last_ai_move = list1[-1]
        x, y = last_ai_move
        move_hint = Circle(Point(GRID_WIDTH * x, GRID_WIDTH * y + EXTRA_HEIGHT), 16)
        move_hint.setOutline('red')
        move_hint.setWidth(3)
        move_hint.draw(win)
    player_wins = False


def main():
    win = gomokuwin()  # 游戏对象

    for i in range(COLUMN + 1):
        for j in range(ROW + 1):
            list_all.append((i, j))  # list_all表示所有可进行落子的点

    if set_difficulty(win):  # 初始时先进行难度设置，返回值为真时表示点击了退出游戏
        return

    change = 0  # 表示当前的落子方

    global move_hint  # 落子提示

    while 1:
        if change % 2 == 1:
            # 如果玩家或AI已经获胜，则跳过当前回合
            if game_win(list1) or game_win(list2):
                change += 1
                continue
            # 调用AI算法找到下一步落子位置
            pos = a_star_search()

            # 如果落子位置为 None，表示没有可落子的位置，游戏平局（该分支未测试）
            if pos is None:
                messagebox.showinfo("游戏结束", "平局！")
                change += 1
                break

            # 移除上一个玩家的落子提示圆圈（如果存在的话）
            if move_hint:
                move_hint.undraw()

            # 将AI的落子位置加入列表中
            list1.append(pos)
            list3.append(pos)

            # 创建新的落子并设置为落子提示
            piece = Circle(Point(GRID_WIDTH * pos[0], GRID_WIDTH * pos[1] + EXTRA_HEIGHT), 16)
            piece.setFill('white')
            piece.draw(win)
            move_hint = Circle(Point(GRID_WIDTH * pos[0], GRID_WIDTH * pos[1] + EXTRA_HEIGHT), 16)  # 创建落子提示圆圈
            move_hint.setOutline('red')  # 设置提示圆圈颜色为红色
            move_hint.setWidth(3)  # 设置提示圆圈的线宽
            move_hint.draw(win)  # 绘制落子提示圆圈

            # 如果AI获胜，则显示游戏结束信息
            if game_win(list1):
                messagebox.showinfo("游戏结束", "你输了！")
            change += 1

        else:
            click_point = win.getMouse()  # 获得当前鼠标点击的点坐标
            x = click_point.getX()
            y = click_point.getY()

            # 检查点击位置是否在按钮区域内
            if 10 <= x <= 90 and 10 <= y <= EXTRA_HEIGHT - 10:
                # 重置游戏
                if reset_game(win):
                    return
                continue
            elif 110 <= x <= 190 and 10 <= y <= EXTRA_HEIGHT - 10:
                # 悔棋
                undo_move(win)
                continue
            elif 210 <= x <= 290 and 10 <= y <= EXTRA_HEIGHT - 10:
                # 退出游戏
                win.close()
                return

            # 如果点击事件在棋盘区域内且位置合法(落子点未存在棋子)
            elif y > EXTRA_HEIGHT and not (
                    (round(x / GRID_WIDTH), round((y - EXTRA_HEIGHT) / GRID_WIDTH)) in list3) and not game_win(
                list1) and not game_win(list2):
                # 记录棋子
                a2 = round(x / GRID_WIDTH)
                b2 = round((y - EXTRA_HEIGHT) / GRID_WIDTH)
                list2.append((a2, b2))
                list3.append((a2, b2))

                # 移除上一个玩家的落子提示圆圈（如果有的话）
                if move_hint:
                    move_hint.undraw()

                # # 创建新的落子并设置为落子提示
                piece = Circle(Point(GRID_WIDTH * a2, GRID_WIDTH * b2 + EXTRA_HEIGHT), 16)
                piece.setFill('black')
                piece.draw(win)

                move_hint = Circle(Point(GRID_WIDTH * a2, GRID_WIDTH * b2 + EXTRA_HEIGHT), 16)  # 创建落子提示圆圈
                move_hint.setOutline('red')  # 设置提示圆圈颜色为红色
                move_hint.setWidth(3)  # 设置提示圆圈的线宽
                move_hint.draw(win)  # 绘制落子提示圆圈

                # 如果玩家获胜，则显示游戏结束信息
                if game_win(list2):
                    messagebox.showinfo("游戏结束", "你赢了！")
                    global player_wins
                    player_wins = True
                change += 1
    win.close()


main()
